/// <license>
/// This software and are provided as-is and without  warranty. 
/// This is  free  software,  as long  as you  clearly  display
/// the original author and his company for at least  1  second
/// on the attached display. If you need a commercial   license 
/// in which you do not have these restrictions, please contact
/// us via our website.
/// 
/// Author: Mario Di Vece
/// Unosquare S.A. de C.V. Copyright (c) 2013
/// url: http://www.unosquare.com
/// </license>

namespace Unosquare.Labs.Drivers.Vizic
{
    using Microsoft.SPOT;
    using SecretLabs.NETMF.Hardware.Netduino;
    using System;
    using System.IO.Ports;
    using Unosquare.Labs.Utils;

    /// <summary>
    /// Driver for Vizic Technologies Smart GPU - Software Version 2.0
    /// NOTE: Prior software versions have different buttons, and reduced file commands
    /// </summary>
    public class VizicSmartGpu : IDisposable
    {

        #region Constants

        private const int InitialBaudRate = 9600;
        private const int ResponseTimeout = 1000;
        private const int InitDelay = 500;
        private const int SerialBufferDelay = 5;
        private const int TouchPositionTimeout = 30;
        private const int TouchPollInterval = 2;

        #endregion

        #region Constructor and IDisposable implementation

        /// <summary>
        /// Initializes a new instance of the <see cref="VizicSmartGpu"/> class.
        /// </summary>
        /// <param name="serialPortName">Name of the serial port.</param>
        /// <exception cref="System.SystemException">Could not connect to Device 'Vizic Smart GPU'</exception>
        public VizicSmartGpu(string serialPortName = "COM1")
        {
            this.SerialPort = new SerialPort(serialPortName, InitialBaudRate, Parity.None, 8, StopBits.One);
            this.SerialPort.Open();

            var tries = 0;
            while (true)
            {
                var response = this.LcdInitialize();
                if (response == VizicResponseCodes.Success)
                    break;

                tries++;
                if (tries >= 10)
                    throw new SystemException("Could not connect to Device 'Vizic Smart GPU'");

                System.Threading.Thread.Sleep(InitDelay);
            }

            this.ForegroundColor = VizicColor.White;
            this.BackgroundColor = VizicColor.Black;
            this.FillBackground = false;
            
            // Please read the license of this driver carefully.
            // If you need to remove the DisplayCredits call, tyou will need to
            // obtain a commercial license.
            this.DisplayCredits();
        }

        private void DisplayCredits()
        {
            this.ClearScreen();
            this.Brightness = 64;
            this.ClearScreen();
            this.WriteString(0, 0, this.HorizontalResolution, 12, VizicFontSizes.Size10, "Vizic Smart GPU - Display Driver v1.0");
            this.WriteString(0, 12, this.HorizontalResolution, 24, VizicFontSizes.Size10, "Author: Mario Di Vece");
            this.WriteString(0, 24, this.HorizontalResolution, 36, VizicFontSizes.Size10, "Plase visit unosquare.com");
            System.Threading.Thread.Sleep(1000);
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// In this case, it stops the touch event loop, closes, and marks the the serial port object for garbage collection.
        /// </summary>
        public void Dispose()
        {
            if (TouchEventingEnabled)
                this.TouchEventingEnabled = false;

            if (this.SerialPort.IsOpen)
                this.SerialPort.Close();

            this.SerialPort = null;
        }

        #endregion

        #region Instance Members, Delegates, Events, and Control Variables

        public delegate void TouchDataEventDelegate(object sender, VizicTouchEventArgs e);
        public delegate void TouchPositionDownEventDelegate(object sender, VizicTouchEventArgs e);
        public delegate void TouchPositionDragEventDelegate(object sender, VizicDragEventArgs e);
        public delegate void TouchPositionUpEventDelegate(object sender, VizicTouchEventArgs e);

        /// <summary>
        /// Occurs when touch data is available.
        /// </summary>
        public event TouchDataEventDelegate TouchData;
        /// <summary>
        /// Occurs when the touch screen is pressed down.
        /// </summary>
        public event TouchPositionDownEventDelegate TouchPositionDown;
        /// <summary>
        /// Occurs when the touch screen poisition is dragged.
        /// </summary>
        public event TouchPositionDragEventDelegate TouchPositionDrag;
        /// <summary>
        /// Occurs when the touch screen is released.
        /// </summary>
        public event TouchPositionUpEventDelegate TouchPositionUp;

        // Control variable for locking Serial Port Send and Receive operations in multithreaded scenarios
        // private object SyncRoot = new object();

        // Control variables for multithreaded touch input
        protected bool TouchEventsEnabled = false;
        protected bool TouchThreadRunning = false;

        // Property-backing variables
        protected bool m_SleepMode = false;
        protected byte m_Brightness = 255;
        protected VizicBaudRates m_BaudRate = VizicBaudRates.B9600;
        protected VizicOrientations m_Orientation = VizicOrientations.HorizontalIL;
        protected bool m_D0;
        protected bool m_D1;
        protected VizicColor LastClearScreenColor = null; // so we avoid calling the "set background color" if already in place.

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the serial port.
        /// </summary>
        /// <value>
        /// The serial port.
        /// </value>
        protected SerialPort SerialPort
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the color of the background for text and the clear screen commands
        /// </summary>
        /// <value>
        /// The color of the background.
        /// </value>
        public VizicColor BackgroundColor { get; set; }

        /// <summary>
        /// Gets or sets the color of the lines, shapres and text
        /// </summary>
        /// <value>
        /// The color of the foreground.
        /// </value>
        public VizicColor ForegroundColor { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether backgrounds are filled when drawing shapes or text
        /// </summary>
        /// <value>
        ///   <c>true</c> if [fill background]; otherwise, <c>false</c>.
        /// </value>
        public bool FillBackground { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether the device is in sleep mode
        /// </summary>
        /// <value>
        ///   <c>true</c> if [sleep mode]; otherwise, <c>false</c>.
        /// </value>
        public bool SleepMode
        {
            get
            {
                return this.m_SleepMode;
            }
            set
            {
                if (this.LcdSleep(this.m_SleepMode) == VizicResponseCodes.Success)
                    this.m_SleepMode = value;
                else
                    return;

                if (value)
                    this.LcdSetBrightness(0);
                else
                    this.LcdSetBrightness(this.m_Brightness);
            }
        }

        /// <summary>
        /// Gets or sets the brightness of the LCD.
        /// </summary>
        /// <value>
        /// The brightness.
        /// </value>
        public byte Brightness
        {
            get { return this.m_Brightness; }
            set
            {
                if (this.LcdSetBrightness(this.m_Brightness) == VizicResponseCodes.Success)
                    this.m_Brightness = value;
            }
        }

        /// <summary>
        /// Gets or sets the orientation of the coordinates for the device.
        /// </summary>
        /// <value>
        /// The orientation.
        /// </value>
        public VizicOrientations Orientation
        {
            get { return this.m_Orientation; }
            set
            {
                if (this.LcdSetOrientation(this.m_Orientation) == VizicResponseCodes.Success)
                    this.m_Orientation = value;
            }
        }

        /// <summary>
        /// Gets the vertical resolution, depending on the orientation.
        /// </summary>
        /// <value>
        /// The vertical resolution.
        /// </value>
        public ushort VerticalResolution
        {
            get
            {
                if (this.Orientation == VizicOrientations.HorizontalIL || this.Orientation == VizicOrientations.HorizontalIR)
                    return 240;
                else
                    return 320;
            }
        }

        /// <summary>
        /// Gets the horizontal resolution, depending on the orientation.
        /// </summary>
        /// <value>
        /// The horizontal resolution.
        /// </value>
        public ushort HorizontalResolution
        {
            get
            {
                if (this.Orientation == VizicOrientations.HorizontalIL || this.Orientation == VizicOrientations.HorizontalIR)
                    return 320;
                else
                    return 240;
            }
        }

        /// <summary>
        /// Gets or sets the baud rate.
        /// </summary>
        /// <value>
        /// The baud rate.
        /// </value>
        public VizicBaudRates BaudRate
        {
            get
            {
                return this.m_BaudRate;
            }
            set
            {
                if (this.LcdSetBaudRate(value) == VizicResponseCodes.Success)
                    this.m_BaudRate = value;
            }
        }

        /// <summary>
        /// Sets a value for the D0 pin (3v3 logic)
        /// </summary>
        /// <value>
        ///   <c>true</c> if d0; otherwise, <c>false</c>.
        /// </value>
        public bool D0
        {
            get
            {
                return this.m_D0;
            }
            set
            {
                if (DigitalOutPin(DigitalPins.D0, value) == VizicResponseCodes.Success)
                    this.m_D0 = value;
            }
        }

        /// <summary>
        /// Sets a value for the D1 pin (3v3 logic)
        /// </summary>
        /// <value>
        ///   <c>true</c> if d0; otherwise, <c>false</c>.
        /// </value>
        public bool D1
        {
            get
            {
                return this.m_D1;
            }
            set
            {
                if (DigitalOutPin(DigitalPins.D1, value) == VizicResponseCodes.Success)
                    this.m_D1 = value;
            }
        }

        #endregion

        #region Enumerated Definitions

        /// <summary>
        /// Defines 1-byte commands
        /// </summary>
        protected enum Commands : byte
        {

            // Basic Commands
            Initialize = 0x55,
            SetBackgroundColor = 0x42,
            EraseScreen = 0x45,
            SetBrightness = 0x56,
            Sleep = 0x5A,
            SetOrientation = 0x4f,
            SetBaudRate = 0x58,

            // Graphics Commands
            PutPixel = 0x50,
            DrawLine = 0x4c,
            DrawRectangle = 0x52,
            DrawCircle = 0x43,
            DrawTriangle = 0x54,
            DrawImage = 0x49,

            // Text Commands
            SetTextBackground = 0x41,
            PutLetter = 0x57,
            DisplayString = 0x53,

            // Touch Commands
            GetTouch = 0x47,
            CalibrateTouch = 0x48,

            // Memory Commands
            MemoryRead = 0x4d,
            DigitalOutPin = 0x44,
        }

        /// <summary>
        /// Defines the second byte of the SD commands
        /// </summary>
        protected enum SDCommands : byte
        {
            // SD Commands
            ImageSD = 0x49,
            StringSD = 0x53,
        }

        /// <summary>
        /// Defines the second byte of File Commands
        /// </summary>
        protected enum FileCommands : byte
        {
            OpenFile = 0x4f,
            CloseFile = 0x43,
            SyncFile = 0x53,
            SetPointer = 0x50,
            ReadFile = 0x52,
            WriteFile = 0x57,
            ListFiles = 0x48
        }

        /// <summary>
        /// Defines the Digital output pins available on the device
        /// </summary>
        protected enum DigitalPins : byte
        {
            D0 = 0x00,
            D1 = 0x01
        }

        #endregion

        #region Generic Issue and Wait For Response Methods

        /// <summary>
        /// Sends the payload via the current COM Port
        /// </summary>
        /// <param name="payload">The payload.</param>
        /// <param name="forceFlush">if set to <c>true</c> [force flush].</param>
        protected void SerialSend(byte[] payload, bool forceFlush = true)
        {
            this.SerialPort.Write(payload, 0, payload.Length);
            if (forceFlush) this.SerialPort.Flush();

            //Debug.Print("TX: (" + payload.Length + ") " + payload.CreateString());
        }

        /// <summary>
        /// Waits for response on the current COM Port
        /// For variable response lengths, use -1 as the known response length.
        /// </summary>
        /// <param name="knownResponseLength">Length of the known response.</param>
        /// <returns></returns>
        protected byte[] SerialReceive(int knownResponseLength)
        {

            var lastReceptionTime = DateTime.Now;
            var bytesToRead = 0;
            var response = new byte[0];
            long elapsedMillis = 0;

            // Wait until data is available from the serial port or we have a timeout
            while (true)
            {
                bytesToRead = this.SerialPort.BytesToRead;
                if (bytesToRead > 0) break;
                elapsedMillis = DateTime.Now.Subtract(lastReceptionTime).TotalMilliseconds();
                var hasTimedOut = elapsedMillis > ResponseTimeout;
                if (hasTimedOut)
                    throw new SystemException("Timed out at " + elapsedMillis + "ms");
            }

            // Read the response
            bytesToRead = this.SerialPort.BytesToRead;
            while (bytesToRead > 0 || response.Length < knownResponseLength)
            {
                var partialResponse = new byte[bytesToRead];
                this.SerialPort.Read(partialResponse, 0, bytesToRead);
                response = response.Append(partialResponse);
                if (knownResponseLength > 0
                    && response.Length >= knownResponseLength)
                    break;

                System.Threading.Thread.Sleep(SerialBufferDelay); // give it a break so the buffer fills up.
                bytesToRead = SerialPort.BytesToRead;
            }

            //Debug.Print("RX: (" + response.Length + ") " + response.CreateString());
            return response;
        }

        /// <summary>
        /// Combines a Serial Send and a Serial Receive operation
        /// </summary>
        /// <param name="command">The command.</param>
        /// <param name="arguments">The arguments.</param>
        /// <param name="knownResponseLength">Length of the known response.</param>
        /// <returns></returns>
        protected byte[] SendAndReceive(Commands command, byte[] arguments, int knownResponseLength)
        {
            var totalByteLength = 1 + arguments.Length;
            var payload = new byte[totalByteLength];
            payload[0] = (byte)command;

            for (int i = 1; i <= arguments.Length; i++)
                payload[i] = arguments[i - 1];

            this.SerialSend(payload);
            var response = SerialReceive(knownResponseLength);

            return response;
        }

        /// <summary>
        /// Issues a command in a simple way, expecting a response of 1 byte
        /// </summary>
        /// <param name="command">The command.</param>
        /// <returns></returns>
        protected VizicResponseCodes IssueCommand(Commands command)
        {
            var stub = new byte[0];
            var response = this.SendAndReceive(command, stub, 1);
            return (VizicResponseCodes)response[response.Length - 1];
        }

        /// <summary>
        /// Issues a command in a simple way, expecting a response of 1 byte
        /// </summary>
        /// <param name="command">The command.</param>
        /// <param name="argument">The argument.</param>
        /// <returns></returns>
        protected VizicResponseCodes IssueCommand(Commands command, byte argument)
        {
            var stub = new byte[] { argument };
            var response = this.SendAndReceive(command, stub, 1);
            return (VizicResponseCodes)response[response.Length - 1];
        }

        /// <summary>
        /// Issues a command in a simple way, expecting a response of 1 byte
        /// </summary>
        /// <param name="command">The command.</param>
        /// <param name="argument">The argument.</param>
        /// <returns></returns>
        protected VizicResponseCodes IssueCommand(Commands command, byte[] argument)
        {
            var response = this.SendAndReceive(command, argument, 1);
            return (VizicResponseCodes)response[response.Length - 1];
        }

        /// <summary>
        /// Issues a command in a simple way, expecting a variable-length response.
        /// </summary>
        /// <param name="command">The command.</param>
        /// <param name="argument">The argument.</param>
        /// <param name="response">The response.</param>
        /// <returns></returns>
        protected VizicResponseCodes IssueCommand(Commands command, byte[] argument, out byte[] response)
        {
            response = this.SendAndReceive(command, argument, -1);
            return (VizicResponseCodes)response[response.Length - 1];
        }

        #endregion

        #region Basic LCD Commands

        /// <summary>
        /// Initializes the LCD. This method is called within the constructor, and that is why it is protected.
        /// </summary>
        /// <returns></returns>
        protected VizicResponseCodes LcdInitialize()
        {
            return this.IssueCommand(Commands.Initialize);
        }

        /// <summary>
        /// Clears the screen with the current Background Color
        /// This is a combination of 2 commands
        /// </summary>
        /// <returns></returns>
        public VizicResponseCodes ClearScreen()
        {
            var firstResponse = VizicResponseCodes.Success;

            if (LastClearScreenColor == null || LastClearScreenColor.HiColor != BackgroundColor.HiColor)
                firstResponse = LcdSetBackgroundColor(this.BackgroundColor);

            var secondResponse = VizicResponseCodes.Failure;
            if (firstResponse == VizicResponseCodes.Success)
                secondResponse = LcdEraseScreen();

            return secondResponse;
        }

        /// <summary>
        /// Sets the background color. You will need to call the LcdEraseScreen method after this command.
        /// </summary>
        /// <returns></returns>
        protected VizicResponseCodes LcdSetBackgroundColor(VizicColor color)
        {
            this.LastClearScreenColor = color;
            var argument = color.HiColor;
            return IssueCommand(Commands.SetBackgroundColor, argument);
        }

        /// <summary>
        /// Clears the screen with the previously-specified background color.
        /// </summary>
        /// <returns></returns>
        protected VizicResponseCodes LcdEraseScreen()
        {
            return IssueCommand(Commands.EraseScreen);
        }

        /// <summary>
        /// Sets the brightness of the LCD. Level goes from 0 to 255.
        /// </summary>
        /// <param name="level">The level.</param>
        /// <returns></returns>
        protected VizicResponseCodes LcdSetBrightness(byte level)
        {
            byte normalizedLevel = (byte)(level * 128 / 255);
            return IssueCommand(Commands.SetBrightness, normalizedLevel);
        }

        /// <summary>
        /// Takes the LCD into or out of Sleep mode.
        /// Set the brightness to 0 for additional power svaings.
        /// </summary>
        /// <param name="doSleep">if set to <c>true</c> [do sleep].</param>
        /// <returns></returns>
        protected VizicResponseCodes LcdSleep(bool doSleep)
        {
            var arg = (byte)(doSleep ? 0x01 : 0x00);
            return IssueCommand(Commands.Sleep, arg);
        }

        /// <summary>
        /// Sets the orientation of the coordinate system.
        /// </summary>
        /// <param name="orientation">The orientation.</param>
        /// <returns></returns>
        protected VizicResponseCodes LcdSetOrientation(VizicOrientations orientation)
        {
            return IssueCommand(Commands.SetOrientation, (byte)orientation);
        }

        /// <summary>
        /// Sets the baud rate of the LCD and the serial port
        /// </summary>
        /// <param name="baudRate">The baud rate.</param>
        /// <returns></returns>
        protected VizicResponseCodes LcdSetBaudRate(VizicBaudRates baudRate)
        {
            var hostBaudRate = InitialBaudRate;
            switch (baudRate)
            {
                case VizicBaudRates.B9600: hostBaudRate = 9600; break;
                case VizicBaudRates.B19200: hostBaudRate = 19200; break;
                case VizicBaudRates.B57600: hostBaudRate = 57600; break;
                case VizicBaudRates.B115200: hostBaudRate = 115200; break;
                case VizicBaudRates.B256K: hostBaudRate = 256000; break;
                case VizicBaudRates.B500K: hostBaudRate = 500000; break;
                case VizicBaudRates.B1M: hostBaudRate = 1000000; break;
                case VizicBaudRates.B2M: hostBaudRate = 2000000; break;
            }

            var firstResponse = IssueCommand(Commands.SetBaudRate, (byte)baudRate);

            var secondResponse = VizicResponseCodes.Failure;
            if (firstResponse == VizicResponseCodes.Success)
            {
                this.SerialPort.Close();
                this.SerialPort.BaudRate = hostBaudRate;
                this.SerialPort.Open();
                System.Threading.Thread.Sleep(InitDelay);
                secondResponse = (VizicResponseCodes)SerialReceive(1)[0];
            }

            return secondResponse;
        }

        #endregion

        #region Graphics Commands

        /// <summary>
        /// Draws a pixel.
        /// </summary>
        /// <returns></returns>
        public VizicResponseCodes DrawPixel(ushort x, ushort y)
        {
            var payload = new byte[6];
            var xV = x.ToByteArray();
            var yV = y.ToByteArray();

            payload.Ingest(xV, 0);
            payload.Ingest(yV, 2);
            payload.Ingest(ForegroundColor.HiColor, 4);

            return IssueCommand(Commands.PutPixel, payload);
        }

        /// <summary>
        /// Draws a line.
        /// </summary>
        /// <param name="x1">The x1.</param>
        /// <param name="y1">The y1.</param>
        /// <param name="x2">The x2.</param>
        /// <param name="y2">The y2.</param>
        /// <returns></returns>
        public VizicResponseCodes DrawLine(ushort x1, ushort y1, ushort x2, ushort y2)
        {
            var payload = new byte[10];
            var x1V = x1.ToByteArray();
            var y1V = y1.ToByteArray();
            var x2V = x2.ToByteArray();
            var y2V = y2.ToByteArray();

            payload.Ingest(x1V, 0);
            payload.Ingest(y1V, 2);
            payload.Ingest(x2V, 4);
            payload.Ingest(y2V, 6);
            payload.Ingest(ForegroundColor.HiColor, 8);

            return IssueCommand(Commands.DrawLine, payload);
        }

        /// <summary>
        /// Draws a rectangle.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <returns></returns>
        public VizicResponseCodes DrawRectangle(ushort x, ushort y, ushort width, ushort height)
        {
            ushort x2 = (ushort)(x + width - 1);
            ushort y2 = (ushort)(y + height - 1);

            var payload = new byte[11];
            var x1V = x.ToByteArray();
            var y1V = y.ToByteArray();
            var x2V = x2.ToByteArray();
            var y2V = y2.ToByteArray();

            payload.Ingest(x1V, 0);
            payload.Ingest(y1V, 2);
            payload.Ingest(x2V, 4);
            payload.Ingest(y2V, 6);
            payload.Ingest(ForegroundColor.HiColor, 8);

            payload[10] = FillBackground ? (byte)0x01 : (byte)0x00;

            return IssueCommand(Commands.DrawRectangle, payload);
        }

        /// <summary>
        /// Draws a circle.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <param name="radius">The radius.</param>
        /// <returns></returns>
        public VizicResponseCodes DrawCircle(ushort x, ushort y, ushort radius)
        {
            var payload = new byte[9];
            var xV = x.ToByteArray();
            var yV = y.ToByteArray();
            var rV = radius.ToByteArray();

            payload.Ingest(xV, 0);
            payload.Ingest(yV, 2);
            payload.Ingest(rV, 4);
            payload.Ingest(ForegroundColor.HiColor, 6);

            payload[8] = FillBackground ? (byte)0x01 : (byte)0x00;

            return IssueCommand(Commands.DrawCircle, payload);
        }

        /// <summary>
        /// Draws a triangle.
        /// </summary>
        /// <param name="x1">The x1.</param>
        /// <param name="y1">The y1.</param>
        /// <param name="x2">The x2.</param>
        /// <param name="y2">The y2.</param>
        /// <param name="x3">The x3.</param>
        /// <param name="y3">The y3.</param>
        /// <returns></returns>
        public VizicResponseCodes DrawTriangle(ushort x1, ushort y1, ushort x2, ushort y2, ushort x3, ushort y3)
        {
            var payload = new byte[15];
            var x1V = x1.ToByteArray();
            var y1V = y1.ToByteArray();
            var x2V = x2.ToByteArray();
            var y2V = y2.ToByteArray();
            var x3V = x3.ToByteArray();
            var y3V = y3.ToByteArray();

            payload.Ingest(x1V, 0);
            payload.Ingest(y1V, 2);
            payload.Ingest(x2V, 4);
            payload.Ingest(y2V, 6);
            payload.Ingest(x3V, 8);
            payload.Ingest(y3V, 10);
            payload.Ingest(ForegroundColor.HiColor, 12);

            payload[14] = FillBackground ? (byte)0x01 : (byte)0x00;
            return IssueCommand(Commands.DrawTriangle, payload);
        }

        /// <summary>
        /// Draws the image.
        /// </summary>
        /// <param name="x">The x1.</param>
        /// <param name="y">The y1.</param>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <param name="rgbData">The RGB data.</param>
        /// <returns></returns>
        /// <exception cref="System.InvalidOperationException">RGB data must be  + expectedLength +  in length.</exception>
        public VizicResponseCodes DrawImage(ushort x, ushort y, ushort width, ushort height, byte[] rgbData)
        {
            ushort x2 = (ushort)(x + width - 1);
            ushort y2 = (ushort)(y + height - 1);

            var pixelCount = (x2 - x + 1) * (y2 - y + 1);
            var expectedLength = pixelCount * 3;

            if (rgbData.Length != expectedLength)
                throw new InvalidOperationException("RGB data must be " + expectedLength + " in length.");

            var payload = new byte[10];
            payload[0] = (byte)Commands.DrawImage;
            payload[1] = 0x00;

            var x1V = x.ToByteArray();
            var y1V = y.ToByteArray();
            var x2V = x2.ToByteArray();
            var y2V = y2.ToByteArray();

            payload.Ingest(x1V, 2);
            payload.Ingest(y1V, 4);
            payload.Ingest(x2V, 6);
            payload.Ingest(y2V, 8);

            SerialSend(payload, false);

            for (int pixelIndex = 0; pixelIndex < expectedLength; pixelIndex += 3)
            {
                var hiColorPixel = VizicColor.ComputeHiColor(rgbData, pixelIndex); // new VizicColor(rgbData.Slice(pixelIndex, 3)); // rgbData[pixelIndex], rgbData[pixelIndex + 1], rgbData[pixelIndex + 2]);
                SerialSend(hiColorPixel, false);
            }

            this.SerialPort.Flush(); // we need to flush manually
            return (VizicResponseCodes)SerialReceive(1)[0];

        }

        #endregion

        #region Text Commands

        /// <summary>
        /// Sets the background color of text operations
        /// </summary>
        /// <param name="color">The color.</param>
        /// <returns></returns>
        protected VizicResponseCodes TextSetBackground(VizicColor color)
        {
            return IssueCommand(Commands.SetTextBackground, color.HiColor);
        }

        /// <summary>
        /// Writes a character
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <param name="size">The size.</param>
        /// <param name="letter">The letter.</param>
        /// <returns></returns>
        public VizicResponseCodes WriteChar(ushort x, ushort y, VizicFontSizes size, char letter)
        {

            var payload = new byte[9];
            payload.Ingest(x.ToByteArray(), 0);
            payload.Ingest(y.ToByteArray(), 2);
            payload.Ingest(ForegroundColor.HiColor, 4);
            payload[6] = (byte)size;
            payload[7] = (byte)(FillBackground ? 0x01 : 0x00);
            payload[8] = (byte)letter;

            if (FillBackground)
                this.TextSetBackground(this.BackgroundColor);

            return IssueCommand(Commands.PutLetter, payload);
        }

        /// <summary>
        /// Writes a string
        /// </summary>
        /// <param name="x1">The x1.</param>
        /// <param name="y1">The y1.</param>
        /// <param name="x2">The x2.</param>
        /// <param name="y2">The y2.</param>
        /// <param name="size">The size.</param>
        /// <param name="text">The text.</param>
        /// <returns></returns>
        public VizicResponseCodes WriteString(ushort x1, ushort y1, ushort x2, ushort y2, VizicFontSizes size, string text)
        {
            var payload = new byte[13];
            payload[0] = 0x00;
            payload.Ingest(x1.ToByteArray(), 1);
            payload.Ingest(y1.ToByteArray(), 3);
            payload.Ingest(x2.ToByteArray(), 5);
            payload.Ingest(y2.ToByteArray(), 7);
            payload.Ingest(ForegroundColor.HiColor, 9);
            payload[11] = (byte)size;
            payload[12] = (byte)(FillBackground ? 0x01 : 0x00);

            var args = new byte[payload.Length + text.Length + 1];
            args.Ingest(payload, 0);
            args.Ingest(text.ToByteArray(), payload.Length);
            args[args.Length - 1] = 0x00;

            if (FillBackground)
                this.TextSetBackground(this.BackgroundColor);

            return IssueCommand(Commands.DisplayString, args);
        }

        #endregion

        #region SD Commands

        /// <summary>
        /// Shows an image stored in the SD card. Do not specify the extension in the filename argument.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <param name="filename">The filename.</param>
        /// <returns></returns>
        public VizicResponseCodes SDShowImage(ushort x, ushort y, string filename)
        {
            var filenameBytes = filename.ToByteArray();
            var payload = new byte[6 + filenameBytes.Length + 1];
            payload[0] = (byte)SDCommands.ImageSD;
            payload[1] = 0x53;

            payload.Ingest(x.ToByteArray(), 2);
            payload.Ingest(y.ToByteArray(), 4);
            payload.Ingest(filenameBytes, 6);
            payload[payload.Length - 1] = 0x00;

            this.SerialSend(payload);
            return (VizicResponseCodes)this.SerialReceive(1)[0];
        }

        /// <summary>
        /// Shows text stored in the SD card. Do not specify the extension in the file argument.
        /// </summary>
        /// <param name="x1">The x1.</param>
        /// <param name="y1">The y1.</param>
        /// <param name="x2">The x2.</param>
        /// <param name="y2">The y2.</param>
        /// <param name="fontSize">Size of the font.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="length">The length.</param>
        /// <param name="filename">The filename.</param>
        /// <returns></returns>
        public VizicResponseCodes SDShowText(ushort x1, ushort y1, ushort x2, ushort y2, VizicFontSizes fontSize, ushort offset, ushort length, string filename)
        {
            var filenameBytes = filename.ToByteArray();
            var payload = new byte[18 + filenameBytes.Length + 1];
            payload[0] = (byte)SDCommands.StringSD;
            payload[1] = 0x53;

            payload.Ingest(x1.ToByteArray(), 2);
            payload.Ingest(y1.ToByteArray(), 4);
            payload.Ingest(x2.ToByteArray(), 6);
            payload.Ingest(y2.ToByteArray(), 8);

            payload.Ingest(ForegroundColor.HiColor, 10);
            payload[12] = (byte)fontSize;
            payload[13] = (byte)(FillBackground ? 0x01 : 0x00);
            payload.Ingest(offset.ToByteArray(), 14);
            payload.Ingest(length.ToByteArray(), 16);

            payload.Ingest(filenameBytes, 18);
            payload[payload.Length - 1] = 0x00;

            this.TextSetBackground(BackgroundColor);
            this.SerialSend(payload);
            return (VizicResponseCodes)this.SerialReceive(1)[0];
        }

        #endregion

        #region File Commands

        /// <summary>
        /// Opens a file in the specified mode.
        /// </summary>
        /// <param name="filename">The filename.</param>
        /// <param name="mode">The mode.</param>
        /// <returns></returns>
        public VizicFileResponseCodes FileOpen(string filename, VizicFileModes mode)
        {
            var filenameBytes = filename.ToByteArray();
            var payload = new byte[3 + filenameBytes.Length + 1];
            payload[0] = 0x46;
            payload[1] = (byte)FileCommands.OpenFile;
            payload[2] = (byte)mode;
            payload.Ingest(filenameBytes, 3);
            payload[payload.Length - 1] = 0x00;

            this.SerialSend(payload);
            return (VizicFileResponseCodes)this.SerialReceive(2)[0];
        }

        /// <summary>
        /// Closes the current file handle.
        /// </summary>
        /// <returns></returns>
        public VizicFileResponseCodes FileClose()
        {
            var payload = new byte[2];
            payload[0] = 0x46;
            payload[1] = (byte)FileCommands.CloseFile;

            this.SerialSend(payload);
            return (VizicFileResponseCodes)this.SerialReceive(2)[0];
        }

        /// <summary>
        /// Flushes the uncommitted data to the current file handle.
        /// </summary>
        /// <returns></returns>
        public VizicFileResponseCodes FileFlush()
        {
            var payload = new byte[2];
            payload[0] = 0x46;
            payload[1] = (byte)FileCommands.SyncFile;

            this.SerialSend(payload);
            return (VizicFileResponseCodes)this.SerialReceive(2)[0];
        }

        /// <summary>
        /// Sets a pointer in the current file handle.
        /// </summary>
        /// <param name="position">The position.</param>
        /// <returns></returns>
        public VizicFileResponseCodes FileSeek(uint position)
        {
            var payload = new byte[6];
            payload[0] = 0x46;
            payload[1] = (byte)FileCommands.SetPointer;
            payload.Ingest(position.ToByteArray(), 2);

            this.SerialSend(payload);
            return (VizicFileResponseCodes)this.SerialReceive(2)[0];
        }

        /// <summary>
        /// Reads the specified number of bytes from the current file handle.
        /// </summary>
        /// <param name="length">The length.</param>
        /// <param name="buffer">The buffer.</param>
        /// <returns></returns>
        public VizicFileResponseCodes FileRead(ushort length, out byte[] buffer)
        {
            buffer = new byte[0];

            var payload = new byte[4];
            payload[0] = 0x46;
            payload[1] = (byte)FileCommands.ReadFile;
            payload.Ingest(length.ToByteArray(), 2);
            this.SerialSend(payload);

            var response = SerialReceive(-1);
            if (response.Length < 4)
                return VizicFileResponseCodes.Failure;

            buffer = response.Slice(0, response.Length - 4);
            return (VizicFileResponseCodes)response[response.Length - 2];
        }

        /// <summary>
        /// Writes the specified buffer to the current file handle.
        /// </summary>
        /// <param name="buffer">The buffer.</param>
        /// <returns></returns>
        public VizicFileResponseCodes FileWrite(byte[] buffer)
        {
            var length = (ushort)buffer.Length;
            var payload = new byte[4 + length];
            payload[0] = 0x46;
            payload[1] = (byte)FileCommands.WriteFile;
            payload.Ingest(length.ToByteArray(), 2);
            payload.Ingest(buffer, 4);

            this.SerialSend(payload);

            var response = SerialReceive(-1);
            if (response.Length < 4)
                return VizicFileResponseCodes.Failure;

            return (VizicFileResponseCodes)response[response.Length - 2];
        }

        /// <summary>
        /// Gets a list of files stored in the root of the SD card.
        /// </summary>
        /// <returns></returns>
        public string[] FileGetList()
        {
            var payload = new byte[1];
            payload[0] = (byte)FileCommands.ListFiles;
            this.SerialSend(payload);

            var response = SerialReceive(-1);
            if (response.Length < 2)
                return null;

            if (response.Length == 2)
                return new string[0];

            var filenames = response.Slice(0, response.Length - 2).CreateString();
            filenames = filenames.TrimEnd(',');
            return filenames.Split(',');
        }

        #endregion

        #region Memory a Digital IO Commands

        /// <summary>
        /// Writes to the specified digital output pin. 
        /// Note that the maximum rate at which you can write is at least 4 times smaller than the current Baud Rate
        /// </summary>
        /// <param name="pin">The pin.</param>
        /// <param name="value">if set to <c>true</c> [value].</param>
        /// <returns></returns>
        protected VizicResponseCodes DigitalOutPin(DigitalPins pin, bool value)
        {
            return IssueCommand(Commands.DigitalOutPin, new byte[] { (byte)pin, (byte)(value ? 0x01 : 0x00) });
        }

        /// <summary>
        /// Snapshots the specified screen area.
        /// Returns a 24-bit color array of bytes (R8G8B8)
        /// </summary>
        /// <param name="x">The x1.</param>
        /// <param name="y">The y1.</param>
        /// <param name="width">The width.</param>
        /// <param name="height">The height.</param>
        /// <returns></returns>
        public byte[] Snapshot(ushort x, ushort y, ushort width, ushort height)
        {
            var expectedLength = width * height * 3 + 1;
            ushort x2 = (ushort)(x + width - 1);
            ushort y2 = (ushort)(y + height - 1);

            var payload = new byte[8];
            payload.Ingest(x.ToByteArray(), 0);
            payload.Ingest(y.ToByteArray(), 2);
            payload.Ingest(x2.ToByteArray(), 4);
            payload.Ingest(y2.ToByteArray(), 6);

            var response = SendAndReceive(Commands.MemoryRead, payload, expectedLength);
            return response;
        }

        #endregion

        #region Touch Commands

        /// <summary>
        /// Polls the Touch Data from the LCD.
        /// </summary>
        /// <returns></returns>
        public VizicTouchResponse TouchGetData()
        {
            var stub = new byte[0];
            var response = SendAndReceive(Commands.GetTouch, new byte[0], 5);
            if (response[response.Length - 1] != (byte)VizicResponseCodes.Success)
                return VizicTouchResponse.NoTouch;
            else
            {
                var touchData = new VizicTouchResponse(response);
                // Make sure X and Y are in the acceptable range -- otherwise, just send a No-Touch response
                //if (System.Math.Max(touchData.X, touchData.Y) > HorizontalResolution)
                //    touchData.ResponseType = VizicSmartGpuTouchResponse.TouchResponseTypes.NoTouchData;

                return touchData;
            }
        }

        /// <summary>
        /// Touches the calibrate.
        /// </summary>
        /// <returns></returns>
        /// <exception cref="System.NotImplementedException">Calibration feature is not implemented</exception>
        public VizicResponseCodes TouchCalibrate()
        {
            throw new NotImplementedException("Calibration feature is not implemented");
        }

        #endregion

        #region Tocuh Input Eventing

        /// <summary>
        /// Raises the <see cref="E:TouchDataEvent" /> event.
        /// </summary>
        /// <param name="e">The <see cref="VizicTouchEventArgs"/> instance containing the event data.</param>
        protected void OnTouchDataEvent(VizicTouchEventArgs e)
        {
            if (TouchData == null) return;
            TouchData(this, e);
        }

        /// <summary>
        /// Raises the <see cref="E:TouchPositionDownEvent" /> event.
        /// </summary>
        /// <param name="e">The <see cref="VizicTouchEventArgs"/> instance containing the event data.</param>
        protected void OnTouchPositionDownEvent(VizicTouchEventArgs e)
        {
            if (TouchPositionDown == null) return;
            TouchPositionDown(this, e);
        }

        /// <summary>
        /// Raises the <see cref="E:TouchPositionDragEvent" /> event.
        /// </summary>
        /// <param name="e">The <see cref="VizicDragEventArgs"/> instance containing the event data.</param>
        protected void OnTouchPositionDragEvent(VizicDragEventArgs e)
        {
            if (TouchPositionDrag == null) return;
            TouchPositionDrag(this, e);
        }

        /// <summary>
        /// Raises the <see cref="E:TouchPositionUpEvent" /> event.
        /// </summary>
        /// <param name="e">The <see cref="VizicTouchEventArgs"/> instance containing the event data.</param>
        protected void OnTouchPositionUpEvent(VizicTouchEventArgs e)
        {
            if (TouchPositionUp == null) return;
            TouchPositionUp(this, e);
        }

        /// <summary>
        /// Runs the touch event loop that enables eventing.
        /// </summary>
        /// <param name="pollIntervalMs">The poll interval ms.</param>
        protected void RunTouchEventLoop(int pollIntervalMs)
        {
            this.TouchEventsEnabled = true;
            this.TouchThreadRunning = true;

            var lastPositionTouchTime = DateTime.Now;
            var lastPositionTouchData = VizicTouchResponse.NoTouch;

            while (TouchEventsEnabled)
            {
                if (pollIntervalMs > 0) System.Threading.Thread.Sleep(pollIntervalMs);
                if (SleepMode) System.Threading.Thread.Sleep(TouchPositionTimeout);

                var currentTouchData = this.TouchGetData();
                switch (currentTouchData.ResponseType)
                {
                    case VizicTouchResponseTypes.NoTouchData:
                        break;
                    case VizicTouchResponseTypes.PositionData:
                        var elpasedWithoutTouch = DateTime.Now.Subtract(lastPositionTouchTime).TotalMilliseconds();
                        var touchEventArgs = new VizicTouchEventArgs(currentTouchData); // for non-drag event
                        this.OnTouchDataEvent(touchEventArgs);

                        // Touch position Down Event
                        if (lastPositionTouchData.ResponseType == VizicTouchResponseTypes.NoTouchData)
                        {
                            elpasedWithoutTouch = 0;
                            OnTouchPositionDownEvent(touchEventArgs);
                        }

                        // Touch Position Up Event
                        if (elpasedWithoutTouch >= TouchPositionTimeout)
                        {
                            lastPositionTouchData.ResponseType = VizicTouchResponseTypes.NoTouchData;
                            OnTouchPositionUpEvent(touchEventArgs);
                        }

                        // Touch Position Drag Event
                        if (lastPositionTouchData.ResponseType == currentTouchData.ResponseType)
                        {
                            var dragEventArgs = new VizicDragEventArgs(currentTouchData, lastPositionTouchData);
                            OnTouchPositionDragEvent(dragEventArgs);
                        }

                        // Update last position response data
                        lastPositionTouchData.X = currentTouchData.X;
                        lastPositionTouchData.Y = currentTouchData.Y;
                        lastPositionTouchData.ResponseType = currentTouchData.ResponseType;
                        lastPositionTouchTime = DateTime.Now;

                        break;
                    default:
                        lastPositionTouchData.ResponseType = VizicTouchResponseTypes.NoTouchData;
                        this.OnTouchDataEvent(new VizicTouchEventArgs(currentTouchData));
                        break;
                }
            }

            this.TouchThreadRunning = false;
        }

        /// <summary>
        /// Gets or sets a value indicating whether touch eventing is enabled.
        /// </summary>
        /// <value>
        /// <c>true</c> if [touch eventing enabled]; otherwise, <c>false</c>.
        /// </value>
        public bool TouchEventingEnabled
        {
            get
            {
                return TouchThreadRunning;
            }
            set
            {
                if (value)
                {
                    if (TouchThreadRunning) return;
                    var touchThread = new System.Threading.Thread(delegate() { RunTouchEventLoop(TouchPollInterval); });
                    touchThread.Start();
                }
                else
                {
                    this.TouchEventsEnabled = false;
                    while (TouchThreadRunning)
                        System.Threading.Thread.Sleep(TouchPollInterval);
                }
            }
        }

        #endregion

    }

}
